<?php

namespace App\Util\Redis;

use Exception;
use Illuminate\Support\Facades\Cache;

/**
 * redis 锁
 */
class RedisLockUtil
{
    /**
     * @var static 单例模式
     */
    protected static $instance;

    /**
     * 单例模式
     */
    public static function getInstance()
    {
        if (static::$instance instanceof static) {
            return static::$instance;
        }
        static::$instance = new static();
        return static::$instance;
    }

    /**
     * @var array
     */
    protected $exceptionCallbackList = [];

    /**
     * @var $redisStore \Illuminate\Cache\RedisStore
     */
    protected $redisStore;
    /**
     * @var $redisClient \Illuminate\Redis\Connections\Connection mixin 了 \Predis\Client。
     *                   同 \Illuminate\Cache\RedisStore 有区别，还是走 laravel 的 cache，若有 cache_prefix，会存在
     * @see \Predis\Client
     */
    protected $redisClient;

    protected $config;

    protected $enable = true;
    /**
     * 单据锁redis key模板
     */
    protected $keyTemplatePrefix = '';

    /**
     * 单据锁默认超时时间（秒）
     */
    protected $defaultExpireTime = 3600;

    public function __construct()
    {
        $redisLockConnection = config('database.redis.lock_connection', 'default');
        $config = config('database.redis.' . $redisLockConnection);
        if (empty($config['lock'])) {
            throw new RedisLockException('缺少 redis lock 配置');
        }

        $lockConfig = $config['lock'];

        $this->config = $lockConfig;
        $this->enable = $lockConfig['enable'];
        $this->keyTemplatePrefix = $lockConfig['key_template_prefix'];
        $this->defaultExpireTime = $lockConfig['default_expire_time'];

        /* @var $redisStore \Illuminate\Cache\RedisStore */
        $this->redisStore = Cache::store('redis');

        $this->redisClient = $this->redisStore->connection();
    }

    /**
     * 加单据锁
     * @param string $key 单据ID
     * @param int $intExpireTime 锁过期时间（分钟）
     * @return bool|int 加锁成功返回唯一锁ID，加锁失败返回false
     */
    public function addLock($key, $intExpireTime = null)
    {
        if (!$this->enable) {
            return true;
        }
        if (empty($key)) {
            return false;
        }

        $intExpireTime = $intExpireTime ?: $this->defaultExpireTime;

        //生成唯一锁ID，解锁需持有此ID
        $value = $this->uuid();

        //根据模板，结合单据ID，生成唯一Redis key（一般来说，单据ID在业务中系统中唯一的）
        $keyTemplate = $this->keyTemplatePrefix . $key;

        $result = $this->redisClient->setnx($keyTemplate, $value);

        if ($result === 1) {
            $this->redisClient->expire($key, $intExpireTime);
            return $value;
        }

        return false;
    }

    /**
     * 解单据锁
     * @param string $key 单据ID
     * @param string $value 锁唯一ID，由 addLock() 返回
     * @return bool
     * @link https://juejin.cn/post/7163834723200925709【PHP+Redis实现分布式锁】
     */
    public function releaseLock($key, $value)
    {
        if (!$this->enable) {
            return true;
        }
        if (empty($key) || empty($value)) {
            throw new RedisLockException('redis lock 缺少解锁必要参数');
        }

        // 防止重复解锁
        $this->clearExceptionCallback();

        $keyTemplate = $this->keyTemplatePrefix . $key;

        $lua = <<<LUA
            local key=KEYS[1]
            local value=ARGV[1]
            if(redis.call('get', key) == value)
            then
                return redis.call('del', key)
            end
LUA;
        // 执行lua脚本
        $result = $this->redisClient->eval($lua, 1, $keyTemplate, $value);
        if ($result !== 1) {
            throw new RedisLockException('redis lock 解锁失败', ['key' => $key, 'value' => $value]);
        }

        return true;
    }

    /**
     * 轮循加锁，到成功为止
     * @param $key
     * @param $addLockTimeout int 加锁超时时间
     * @param $intExpireTime int 缓存时间
     *
     * @return bool|int|string
     * @throws \Exception
     * @author Chris Yu <chrisyu@crabapple.top> 2024/3/26
     */
    public function addLockPolling($key, $addLockTimeout = 30, $intExpireTime = null)
    {
        $startTimestamp = time();
        do {
            $res = $this->addLock($key, $intExpireTime);
            if ($res === false) {
                usleep(200);
                $endTimestamp = time();
                if ($endTimestamp - $startTimestamp >= $addLockTimeout) {
                    throw new RedisLockException('redis 加锁超时');
                }
            }
            else {
                return $res;
            }
        } while (true);
    }

    /**
     * 自动锁
     * @param $callback callable
     * @param $redisKey string 锁名
     * @param $timeout  int 加锁超时时间
     *
     * @author Chris Yu <chrisyu@crabapple.top> 2023/7/4
     */
    public function autoLock(callable $callback, $redisKey, $timeout = 30)
    {
        if (!$this->enable) {
            return $callback();
        }

        // 轮循加锁
        $redisValue = $this->addLock($redisKey, $timeout);
        if ($redisValue === false) {
            return false;
        }

        // 注册异常回调
        $this->registerExceptionCallback(function(\Throwable $e) use ($redisKey, $redisValue) {
            $this->releaseLock($redisKey, $redisValue);
        });

        $callbackResult = $callback();

        // 释放锁
        $this->releaseLock($redisKey, $redisValue);

        return $callbackResult;
    }

    /**
     * 自动轮循锁
     * @param $callback callable
     * @param $redisKey string 锁名
     * @param $timeout int 加锁超时时间
     *
     * @author Chris Yu <chrisyu@crabapple.top> 2023/7/4
     */
    public function autoLockPolling(callable $callback, $redisKey, $timeout = 30)
    {
        if (!$this->enable) {
            return $callback();
        }

        // 轮循加锁
        $lockValue = $this->addLockPolling($redisKey, $timeout);

        // 注册异常回调
        $this->registerExceptionCallback(function(\Throwable $e) use ($redisKey, $lockValue) {
            $this->releaseLock($redisKey, $lockValue);
        });

        $callbackResult = $callback();

        // 释放锁
        $this->releaseLock($redisKey, $lockValue);

        return $callbackResult;
    }

    /**
     * 注册异常回调
     * 如果某个变量需要在异常回调中使用，需要使用use传递，如果该变量需要最新值，则需要引用传递
     * @author Chris Yu <chrisyu@crabapple.top> 2023/4/7
     */
    public function registerExceptionCallback(callable $callback)
    {
        $this->exceptionCallbackList[] = $callback;
    }

    public function triggerExceptionCallback(\Throwable $e)
    {
        if ($this->exceptionCallbackList) {
            foreach ($this->exceptionCallbackList as $exceptionCallback) {
                $exceptionCallback($e);
            }
        }
    }

    public function clearExceptionCallback()
    {
        $this->exceptionCallbackList = [];
    }

    /**
     * 获取全球唯一标识
     * @return string
     */
    protected function uuid()
    {
        return sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff)
        );
    }
}
